﻿//
// Copyright 2008-2011 by Burt Harris
// Licensed under the Microsoft Public License (Ms-PL)
// See: http://www.codeplex.com/yaml/license
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
using Microsoft.Scripting;

namespace YaTools.Yaml.UnitTests
{
    internal enum ParseEvent
    {
        Alias,
        Anchor,
        Tag,
        SequenceStart,
        MappingStart,
        End,
        Null,
        Scalar,
        DocumentStart,
        DocumentEnd,
        StreamEnd
    }

    /// <summary>
    ///This is a test class for YamlParserTest and is intended
    ///to contain all YamlParserTest Unit Tests
    ///</summary>
    [TestClass]
    public class YamlParserTest : CommonTest
    {
        private abstract class TestParser : YamlParser
        {
            protected TestParser() : this(new YamlScanner())
            {
            }

            private TestParser(YamlScanner scanner)
                : base(scanner, new Load())
            {
                Load.Initialize(new TestCompose(this));

                ErrorSink = new RecordErrorSink(this);
                IgnoreWarnings.Add(1);
                IgnoreWarnings.Add(2);
                IgnoreWarnings.Add(308);
            }

            internal readonly HashSet<int> IgnoreWarnings = new HashSet<int>();

            protected class AbortParseException : Exception
            {
            }

            private class RecordErrorSink : ErrorSink
            {
                private readonly TestParser _parser;

                public RecordErrorSink(TestParser parser)
                {
                    _parser = parser;
                }

                public override void Add(string message, string path, string code, string line, SourceSpan span,
                                         int errorCode, Severity severity)
                {
                    if (severity == Severity.Warning && _parser.IgnoreWarnings.Contains(errorCode))
                        return;

                    Trace.WriteLine(message, span.Start + "\tSyntax");
                    var exception = new YamlSyntaxErrorException(
                        message, path, code, line, span, errorCode, severity);
                    _parser.OnEvent(exception);
                    if (severity == Severity.Error || severity == Severity.FatalError)
                        throw new AbortParseException();
                }
            }

            private class TestCompose : Compose
            {
                private readonly TestParser _parser;

                public TestCompose(TestParser parser)
                {
                    _parser = parser;
                }

                private void OnEvent(object e)
                {
                    _parser.OnEvent(e);
                }

                public override Compose BeginMappingNode(Load load)
                {
                    OnEvent(ParseEvent.MappingStart);
                    return this;
                }

                public override Compose BeginSequenceNode(Load load)
                {
                    OnEvent(ParseEvent.SequenceStart);
                    return this;
                }

                public override object AddNullNode(Load load)
                {
                    OnEvent(ParseEvent.Null);
                    return null;
                }

                public override object AddScalarNode(Load load, string value)
                {
                    OnEvent(value);
                    return null;
                }

                public override object AddAliasNode(Load load, string anchor)
                {
                    OnEvent(ParseEvent.Alias);
                    return null;
                }

                public override bool BeginObject(Load load, object value)
                {
                    if (load.Anchor != null)
                        OnEvent(ParseEvent.Anchor);
                    return true;
                }

                public override void EndObject(Load load, object value)
                {
                }

                public override Compose BeginDocument(Load load)
                {
                    OnEvent(ParseEvent.DocumentStart);
                    return this;
                }

                public override object ComposeDocument(Load load)
                {
                    OnEvent(ParseEvent.DocumentEnd);
                    return null;
                }

                public override object ComposeMappingNode(Load load)
                {
                    OnEvent(ParseEvent.End);
                    return null;
                }

                public override object ComposeSequenceNode(Load load)
                {
                    OnEvent(ParseEvent.End);
                    return null;
                }
            }


            protected abstract void OnEvent(object o);
        }

        private class ExpectedParser : TestParser
        {
            private readonly System.Collections.ArrayList _results = new System.Collections.ArrayList();

            public object[] Run()
            {
                try
                {
                    while (ParseDocument())
                    {
                    }
                    _results.Add(ParseEvent.StreamEnd);
                    Trace.WriteLine("");
                }
                catch (AbortParseException)
                {
                    // That's OK, we expecte it...
                }
                return _results.ToArray();
            }

            protected override void OnEvent(object o)
            {
                _results.Add(o);
            }
        }

        private class ActualParser : TestParser
        {
            private bool _backspace;
            private System.Collections.IEnumerator _expectedValueEnumerator;

            private object NextExpectedValue()
            {
                if (_backspace || _expectedValueEnumerator.MoveNext())
                {
                    _backspace = false;
                    return _expectedValueEnumerator.Current;
                }
                return ParseEvent.StreamEnd;
            }

            protected override void OnEvent(object actual)
            {
                object expected = NextExpectedValue();

                // We allow for some sloppyness of hand-created tests...

                // Missing DocumentEnd at end of expected sequence
                if (ParseEvent.DocumentEnd.Equals(actual) && ParseEvent.StreamEnd.Equals(expected))
                    return; 

                // Missing DocumentStart in expected sequence
                if (ParseEvent.DocumentStart.Equals(actual) && !ParseEvent.DocumentStart.Equals(expected))
                {
                    _backspace = true;
                    return;
                }
                
                AssertAreEqual(expected, actual);
            }

            public void Run(params object[] values)
            {
                _expectedValueEnumerator = values.GetEnumerator();
                try
                {
                    try
                    {
                        while (ParseDocument())
                        {
                        }
                        _expectedValueEnumerator = null;
                        Trace.WriteLine("");
                    }
                    catch (SyntaxErrorException ex)
                    {
                        if (_expectedValueEnumerator != null)
                            if (_expectedValueEnumerator.MoveNext())
                            {
                                if (!(ex.Equals(_expectedValueEnumerator.Current)))
                                {
                                    throw;
                                }
                                Trace.WriteLine(ex.Message, "Expected error");
                                // TODO: fixup post-error: continue;
                                return;
                            }
                        throw;
                    }
                    catch (AbortParseException)
                    {
                        if (_expectedValueEnumerator != null)
                            if (_expectedValueEnumerator.MoveNext())
                                Assert.Inconclusive(
                                    "Parse aborted for expected error, but more expected values supplied");
                    }
                }
                catch (UnitTestAssertException ex)
                {
                    Trace.WriteLine(ex.Message);
                    throw;
                }
            }

            //protected override void OnScalar(string component)
            //{
            //    base.OnScalar(component);
            //    object expected = NextExpectedValue();
            //    string expectedString = expected as string;
            //    if (expected == null || expectedString != null) expected = Utility.DebugDisplay(expectedString);
            //    AssertAreEqual(expected, Utility.DebugDisplay(component));
            //}


            private void AssertAreEqual(object expected, object actual)
            {
                if (expected.Equals(actual)) return;
                expected = DisplayFor(expected);
                actual = DisplayFor(actual);

                // Trace output with line breaks to make it easier to compare
                Trace.WriteLine("<EXPECTED/ACTUAL>");
                Trace.WriteLine(expected);
                Trace.WriteLine(actual);
                Trace.WriteLine(this.TokenSpan, "At");

                var msg = string.Format(
                    "EXPECTED: {0}, ACTUAL: {1} at {2}",
                    expected, actual, this.TokenSpan);
                throw new AssertFailedException(msg);
            }

            private static object DisplayFor(object value)
            {
                if (value is string)
                    value = string.Concat("<", Utility.DebugDisplay((string) value), ">");

                var err = value as YamlSyntaxErrorException;
                if (err != null)
                {
                    string at = "";
                    if (err.RawSpan.IsValid)
                        at = " at " + err.RawSpan;
                    value = string.Format("Syntax Error: {0}{1}", err.Message, at);
                }
                return value;
            }
        }

        protected void SimpleStringTest(string yamlText, params object[] values)
        {
            Contract.Requires(yamlText != null);

            try
            {
                SimpleStringTestNoCatch(yamlText, values);
            }
            catch (YamlSyntaxErrorException ex)
            {
                throw new AssertFailedException(ex.Message, ex);
            }
        }

        protected object[] GetExpected(string path)
        {
            Contract.Requires(!string.IsNullOrEmpty(path));

            DebugInput(path, File.ReadAllText(path));
            Debug.WriteLine("Expected results:");

#pragma warning disable 168
            using (var reader = File.OpenText(path))
#pragma warning restore 168
            {
                bool done = false;
                var parser = new ExpectedParser();
                parser.SetSource(reader);
                try
                {
                    var result = parser.Run();
                    done = true;
                    return result;
                }
                catch (Exception ex)
                {
                    throw new AssertFailedException(ex.Message, ex);
                }
                finally
                {
                    if (!done) Trace.WriteLine("Error parsing expected results");
                }
            }
        }

        private readonly HashSet<int> _ignoreWarnings = new HashSet<int>();

        protected void SimpleStringTestNoCatch(string yamlText, params object[] values)
        {
            Contract.Requires(yamlText != null);
            DebugInput("", yamlText);
            var parser = new ActualParser();
            using (var reader = new StringReader(yamlText))
            {
                parser.SetSource(reader);
                parser.IgnoreWarnings.UnionWith(this._ignoreWarnings);
                parser.Run(values);
            }
        }

        protected void FileTest(string path, params object[] values)
        {
            Contract.Requires(!string.IsNullOrEmpty(path));

            DebugInput(path, File.ReadAllText(path));
            Debug.WriteLine("Parse results:");
            using (var reader = File.OpenText(path))
            {
                var parser = new ActualParser();
                parser.SetSource(reader);
                parser.IgnoreWarnings.UnionWith(this._ignoreWarnings);
                parser.Run(values);
                Debug.WriteLine(String.Empty);
            }
        }
    }
}